package com.insight.modules.cas.util;


import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Document;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.StringReader;
import java.util.*;

/**
 * 解析cas,ST验证后的xml
 *
 */
@Slf4j
public final class XmlUtils {

    /**
     * 根据传入的 XML 字符串，创建并返回一个 namespace-aware（支持命名空间）的 DOM Document 对象。
     * 该方法会关闭外部 DTD 加载和启用安全处理特性，避免 XXE 攻击。
     *
     * @param xml XML 字符串内容
     * @return 解析后的 Document 对象
     * @throws RuntimeException 解析出错时抛出异常
     */
    public static Document newDocument(final String xml) {
        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        // 定义安全相关的解析特性配置
        final Map<String, Boolean> features = new HashMap<String, Boolean>();
        // 启用安全处理（防止恶意 XML 攻击）
        features.put(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        // 禁止加载外部DTD，防止XXE攻击
        features.put("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);

        // 逐一设置特性
        for (final Map.Entry<String, Boolean> entry : features.entrySet()) {
            try {
                factory.setFeature(entry.getKey(), entry.getValue());
            } catch (ParserConfigurationException e) {
                log.warn("Failed setting XML feature {}: {}", entry.getKey(), e);
            }
        }
        factory.setNamespaceAware(true); // 支持命名空间

        try {
            if (StringUtils.isEmpty(xml)){
                return null;
            }
            // 解析传入 XML 字符串为 Document 对象
            return factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml)));
        }  catch (IOException e) {
            // 理论上不会发生，因为是内存字符串读取
            log.error("读取 XML 字符串时发生 I/O 错误", e);
            throw new IllegalStateException("XML 解析失败：I/O 异常", e);
        } catch (SAXException e) {
            // 常见：XML 语法错误、格式无效
            log.error("SAX 解析异常 - 可能是无效 XML 格式: {}", e.getMessage(), e);
            throw new IllegalArgumentException("无效的 XML 内容: " + e.getMessage(), e);
        } catch (ParserConfigurationException e) {
            // 属于系统配置错误，非运行时输入问题
            log.error("XML 解析器配置错误", e);
            throw new IllegalStateException("XML 解析器初始化失败", e);
        }
    }

    /**
     * 创建一个 SAX XMLReader 解析器实例。
     * 解析器开启命名空间支持，关闭外部 DTD 加载以防止 XXE 攻击。
     *
     * @return 配置好的 XMLReader 实例
     * @throws RuntimeException 不能创建 XMLReader 时抛出
     */
    public static XMLReader getXmlReader() {
        try {
            final SAXParserFactory spf = SAXParserFactory.newInstance();
            if (spf == null) {
                throw new IllegalStateException("Failed to create SAXParserFactory");
            }
            spf.setNamespaceAware(true);
            spf.setValidating(false);
            final SAXParser saxParser = spf.newSAXParser();
            if (saxParser == null) {
                throw new IllegalStateException("Failed to create SAXParser");
            }
            final XMLReader reader = saxParser.getXMLReader();
            if (reader == null) {
                throw new IllegalStateException("Failed to create XMLReader");
            }
            reader.setFeature("http://xml.org/sax/features/namespaces", true);
            reader.setFeature("http://xml.org/sax/features/namespace-prefixes", false);
            reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
            return reader;
        } catch (ParserConfigurationException e) {
            log.error("SAX 解析器配置失败", e);
            throw new IllegalStateException("XML 解析器创建失败：配置错误", e);
        } catch (SAXException e) {
            log.error("创建 SAX 解析器时发生异常", e);
            throw new IllegalStateException("XML 解析器初始化失败", e);
        }
    }

    /**
     * 解析 XML 字符串，获取所有指定元素名称的文本内容列表。
     * 适合获取多个相同元素的文本，且目前针对两个元素的情况优化。
     *
     * @param xmlAsString XML字符串
     * @param element     需要获取文本的元素名称
     * @return 该元素对应的文本内容列表，解析失败返回 null
     */
    public static List<String> getTextForElements(final String xmlAsString, final String element) {
        final List<String> elements = new ArrayList<>(2);
        final XMLReader reader = getXmlReader();

        // 自定义 SAX 处理器，负责提取元素文本
        final DefaultHandler handler = new DefaultHandler() {
            private boolean foundElement = false;
            private StringBuilder buffer = new StringBuilder();

            @Override
            public void startElement(final String uri, final String localName, final String qName,
                                     final Attributes attributes) throws SAXException {
                if (localName.equals(element)) {
                    this.foundElement = true; // 进入目标元素开始收集文本
                }
            }

            @Override
            public void endElement(final String uri, final String localName, final String qName) throws SAXException {
                if (localName.equals(element)) {
                    this.foundElement = false; // 目标元素结束
                    elements.add(this.buffer.toString()); // 保存文本内容
                    this.buffer = new StringBuilder(); // 重置缓冲区
                }
            }

            @Override
            public void characters(char[] ch, int start, int length) throws SAXException {
                if (this.foundElement) {
                    this.buffer.append(ch, start, length); // 追加文本
                }
            }
        };

        reader.setContentHandler(handler);
        reader.setErrorHandler(handler);

        try {
            if (StringUtils.isEmpty(xmlAsString)){
                return null;
            }
            reader.parse(new InputSource(new StringReader(xmlAsString)));
        } catch (final Exception e) {
            log.error(e.getMessage(), e);
            return null;
        }
        return elements;
    }

    /**
     * 解析 XML 字符串，获取指定元素的文本内容，适用于只存在一个该元素的情况。
     *
     * @param xmlAsString XML字符串
     * @param element     目标元素名称
     * @return 该元素文本内容，解析失败返回 null
     */
    public static String getTextForElement(final String xmlAsString, final String element) {
        final XMLReader reader = getXmlReader();
        final StringBuilder builder = new StringBuilder();

        final DefaultHandler handler = new DefaultHandler() {
            private boolean foundElement = false;

            @Override
            public void startElement(final String uri, final String localName, final String qName,
                                     final Attributes attributes) throws SAXException {
                if (localName.equals(element)) {
                    this.foundElement = true; // 进入目标元素
                }
            }

            @Override
            public void endElement(final String uri, final String localName, final String qName) throws SAXException {
                if (localName.equals(element)) {
                    this.foundElement = false; // 离开目标元素
                }
            }

            @Override
            public void characters(char[] ch, int start, int length) throws SAXException {
                if (this.foundElement) {
                    builder.append(ch, start, length); // 收集文本
                }
            }
        };

        reader.setContentHandler(handler);
        reader.setErrorHandler(handler);

        try {
            if (StringUtils.isEmpty(xmlAsString)){
                return null;
            }
            reader.parse(new InputSource(new StringReader(xmlAsString)));
        } catch (final Exception e) {
            log.error(e.getMessage(), e);
            return null;
        }
        return builder.toString();
    }

    /**
     * 解析 XML 字符串，提取<attributes>标签下的所有子元素和对应的文本内容，
     * 支持单值和多值属性（多值以 List 存储）。
     *
     * @param xml XML字符串
     * @return 解析出的属性键值映射，出错时返回空Map
     */
    public static Map<String, Object> extractCustomAttributes(final String xml) {
        final SAXParserFactory spf = SAXParserFactory.newInstance();
        spf.setNamespaceAware(true);
        spf.setValidating(false);
        try {
            final SAXParser saxParser = spf.newSAXParser();
            if (saxParser == null) {
                return Collections.emptyMap();
            }
            final XMLReader xmlReader = saxParser.getXMLReader();
            if (xmlReader == null) {
                return Collections.emptyMap();
            }
            final CustomAttributeHandler handler = new CustomAttributeHandler();
            xmlReader.setContentHandler(handler);
            if (StringUtils.isNotEmpty(xml)) {
                xmlReader.parse(new InputSource(new StringReader(xml)));
            }
            return handler.getAttributes();
        } catch (final Exception e) {
            log.error(e.getMessage(), e);
            return Collections.emptyMap();
        }
    }

    /**
     * 自定义 SAX Handler，用于提取 <attributes> 标签下的属性名和值。
     */
    private static class CustomAttributeHandler extends DefaultHandler {

        private Map<String, Object> attributes;

        private boolean foundAttributes;

        private String currentAttribute;

        private StringBuilder value;

        @Override
        public void startDocument() throws SAXException {
            this.attributes = new HashMap<>();
        }

        @Override
        public void startElement(final String namespaceURI, final String localName, final String qName,
                                 final Attributes attributes) throws SAXException {
            if ("attributes".equals(localName)) {
                this.foundAttributes = true; // 进入<attributes>标签
            } else if (this.foundAttributes) {
                // 进入某个自定义属性标签，开始收集值
                this.value = new StringBuilder();
                this.currentAttribute = localName;
            }
        }

        @Override
        public void characters(final char[] chars, final int start, final int length) throws SAXException {
            if (this.currentAttribute != null) {
                value.append(chars, start, length); // 追加属性值文本
            }
        }

        @Override
        public void endElement(final String namespaceURI, final String localName, final String qName)
                throws SAXException {
            if ("attributes".equals(localName)) {
                // 离开<attributes>标签，标记结束
                this.foundAttributes = false;
                this.currentAttribute = null;
            } else if (this.foundAttributes) {
                // 结束某个属性标签，保存属性值到Map中
                final Object existing = this.attributes.get(this.currentAttribute);

                if (existing == null) {
                    this.attributes.put(this.currentAttribute, this.value.toString());
                } else {
                    // 如果该属性已存在且是单值，则转为List存储多值
                    final List<Object> items;
                    if (existing instanceof List) {
                        items = (List<Object>) existing;
                    } else {
                        items = new LinkedList<>();
                        items.add(existing);
                        this.attributes.put(this.currentAttribute, items);
                    }
                    items.add(this.value.toString());
                }
            }
        }

        public Map<String, Object> getAttributes() {
            return this.attributes;
        }
    }


    /**
     * main方法演示示例，测试对CAS返回的XML认证成功和失败的解析效果。
     */
    public static void main(String[] args) {
        // 模拟认证成功的CAS响应XML
        String result = "<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>\r\n" +
                "    <cas:authenticationSuccess>\r\n" +
                "        <cas:user>admin</cas:user>\r\n" +
                "        <cas:attributes>\r\n" +
                "            <cas:credentialType>UsernamePasswordCredential</cas:credentialType>\r\n" +
                "            <cas:isFromNewLogin>true</cas:isFromNewLogin>\r\n" +
                "            <cas:authenticationDate>2019-08-01T19:33:21.527+08:00[Asia/Shanghai]</cas:authenticationDate>\r\n" +
                "            <cas:authenticationMethod>RestAuthenticationHandler</cas:authenticationMethod>\r\n" +
                "            <cas:successfulAuthenticationHandlers>RestAuthenticationHandler</cas:successfulAuthenticationHandlers>\r\n" +
                "            <cas:longTermAuthenticationRequestTokenUsed>false</cas:longTermAuthenticationRequestTokenUsed>\r\n" +
                "        </cas:attributes>\r\n" +
                "    </cas:authenticationSuccess>\r\n" +
                "</cas:serviceResponse>";

        // 模拟认证失败的CAS响应XML
        String errorRes = "<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>\r\n" +
                "    <cas:authenticationFailure code=\"INVALID_TICKET\">未能够识别出目标 &#39;ST-5-1g-9cNES6KXNRwq-GuRET103sm0-DESKTOP-VKLS8B3&#39;票根</cas:authenticationFailure>\r\n" +
                "</cas:serviceResponse>";

        // 测试提取失败原因文本
        String error = XmlUtils.getTextForElement(errorRes, "authenticationFailure");
        System.out.println("------" + error);

        // 测试认证成功中提取不存在的失败元素，返回null
        String error2 = XmlUtils.getTextForElement(result, "authenticationFailure");
        System.out.println("------" + error2);

        // 提取认证成功返回的用户名
        String principal = XmlUtils.getTextForElement(result, "user");
        System.out.println("---principal---" + principal);

        // 提取认证成功返回的自定义属性
        Map<String, Object> attributes = XmlUtils.extractCustomAttributes(result);
        System.out.println("---attributes---" + attributes);
    }
}

